#import "BWPathelizer.h"
#import "BWGrid.h"
#import "BWStitch.h"
#import "BWJunkyard.h"

static BWPathelizer *g_defaultPathelizer;

@implementation BWPathelizer

+ (BWPathelizer *) defaultPathelizer
{
    if (g_defaultPathelizer == nil) {
        g_defaultPathelizer = [[BWPathelizer alloc] init];
    }
    
    return (g_defaultPathelizer);

} // defaultPathelizer


// this is pre-rotation
+ (BWPathelizer_StitchMetrics) calculateStitchMetricsWithLength: (float) length
                                                          width: (float) width
                                                          bendX: (float) bendX
                                                          bendY: (float) bendY
                                                     bendSpread: (float) bendSpread
{
    BOOL bendy = NO;

    if (bendX != 0.0 || bendY != 0.0) {
        bendy = YES;
    }

    // center point of the far-end
    NSPoint startCenter;
    startCenter = NSZeroPoint;

    NSPoint endCenter;
    endCenter = NSMakePoint (length, 0.0);

    // calcuate the four points around the rectangle
    NSPoint pt1, pt2;
    NSPoint pt3, pt4;

    pt1 = NSMakePoint (0.0, width);
    pt2 = NSMakePoint (length, width);
    pt3 = NSMakePoint (length, -width);
    pt4 = NSMakePoint (0.0, -width);


    NSPoint controlPoint1 = { pt1.x + bendX + bendSpread, 
                              pt1.y + bendY - width * 2 };
    NSPoint controlPoint2 = { pt2.x - (length - bendX) - bendSpread, 
                              pt2.y + bendY - width * 2 };

    NSPoint controlPoint3 = { pt3.x - (length - bendX) - bendSpread,
                              pt3.y + bendY + 2*width };
    NSPoint controlPoint4 = { pt4.x + bendX + bendSpread,
                              pt4.y + bendY + 2*width };

    float angle1, angle2, angle3, angle4;

    // these are the start and stop angles for the half-circle
    // end points

    if (!bendy) {
        angle1 = 90.0;
        angle2 = 270.0;
        angle3 = 270.0;
        angle4 = 90.0;

    } else {

        // otherwise figure out the angles
        float angle;
        angle = angleForPoints (startCenter, controlPoint1);

        float angleDegrees;
        angleDegrees = radiansToDegrees (angle);
        angle2 = angleDegrees + 270;
        angle1 = angleDegrees + 90;

        angle = degreesToRadians (angle1);

        pt1 = NSMakePoint (cos(angle) * width, 
                           sin(angle) * width);
        pt2 = NSMakePoint (cos(angle - PI) * width,
                           sin(angle - PI) * width);

        angle = angleForPoints (endCenter, controlPoint2);
        angleDegrees = radiansToDegrees (angle);

        angle4 = angleDegrees + 270;
        angle3 = angleDegrees + 90;

        angle = degreesToRadians (angle3);
        pt3 = NSMakePoint (cos(angle) * width + endCenter.x,
                           sin(angle) * width + endCenter.y);

        pt4 = NSMakePoint (cos(angle - PI) * width + endCenter.x,
                           sin(angle - PI) * width + endCenter.y);
    }

    BWPathelizer_StitchMetrics metrics;
    memset (&metrics, 0x00, sizeof(metrics));

    metrics.bendy = bendy;

    metrics.width = width;
    metrics.length = length;
    metrics.startCenter = startCenter;

    metrics.endCenter = endCenter;

    metrics.pt1 = pt1;
    metrics.pt2 = pt2;
    metrics.pt3 = pt3;
    metrics.pt4 = pt4;

    metrics.angle1 = angle1;
    metrics.angle2 = angle2;
    metrics.angle3 = angle3;
    metrics.angle4 = angle4;

    metrics.controlPoint1 = controlPoint1;
    metrics.controlPoint2 = controlPoint2;
    metrics.controlPoint3 = controlPoint3;
    metrics.controlPoint4 = controlPoint4;

    metrics.midpoint.x = (metrics.startCenter.x + metrics.endCenter.x) / 2;
    metrics.midpoint.y = (metrics.startCenter.y + metrics.endCenter.y) / 2;

    if (bendy) {
        // !!! the length / 2.0 is to undo some of the work done by the calling
        // !!! method, so we can get the original bend displacement back
        metrics.midpoint.y += bendY;
        metrics.midpoint.x += bendX - (length / 2.0);
    }

    return (metrics);

} // calculateStitchMetrics


// post-rotation
+ (BWPathelizer_StitchMetrics) calculateMetricsForStitch: (BWStitch *) stitch
                                                  inGrid: (BWGrid *) grid
{
    // Cut and pasted!  argh!

    BWGrid_Square startSquare, endSquare;
    BWGrid_SquarePart startPart, endPart;

    [stitch getStartSquare: &startSquare  part: &startPart];
    [stitch getEndSquare: &endSquare  part: &endPart];

    float stitchWidth;
    stitchWidth = [stitch width];

    NSRect startRect, endRect;
    startRect = [grid rectForSquare: startSquare  part: startPart];
    endRect = [grid rectForSquare: endSquare  part: endPart];

    NSPoint startPoint, endPoint;
    startPoint = centerPoint (startRect);
    endPoint = centerPoint (endRect);

    float length;
    length = distance (startPoint, endPoint);

    // ^^^ ---- cutten pasten ---- ^^^

    // control point fooby
    float bendX = 0.0;
    float bendY = 0.0;
    float bendSpread = 0.0;

    float bendFactor;
    bendFactor = [stitch bendFactor];

    float bendDisplacement;
    bendDisplacement = [stitch bendDisplacement];

    if (bendFactor != 0.0) {
        bendX = length / 2.0 + bendDisplacement;
        bendY = bendFactor;

        bendSpread = [stitch bendFlatness];
    }

    BWPathelizer_StitchMetrics metrics;
    metrics = [self calculateStitchMetricsWithLength: length
                    width: stitchWidth
                    bendX: bendX
                    bendY: bendY
                    bendSpread: bendSpread];

    
    NSAffineTransform *transformer; // robots in disguise
    transformer = [NSAffineTransform transform];
    
    // move the origin to the start point
    [transformer translateXBy: startPoint.x + 0.5  yBy: startPoint.y + 0.5];
    
    float stitchAngle;
    stitchAngle = angleForPoints (startPoint, endPoint);

    [transformer rotateByRadians: stitchAngle];

    metrics.startCenter = [transformer transformPoint: metrics.startCenter];
    metrics.endCenter = [transformer transformPoint: metrics.endCenter];

    metrics.stitchAngle = stitchAngle;

    metrics.pt1 = [transformer transformPoint: metrics.pt1];
    metrics.pt2 = [transformer transformPoint: metrics.pt2];
    metrics.pt3 = [transformer transformPoint: metrics.pt3];
    metrics.pt4 = [transformer transformPoint: metrics.pt3];

    metrics.controlPoint1 
        = [transformer transformPoint: metrics.controlPoint1];
    metrics.controlPoint2 
        = [transformer transformPoint: metrics.controlPoint2];
    metrics.controlPoint3 
        = [transformer transformPoint: metrics.controlPoint3];
    metrics.controlPoint4 
        = [transformer transformPoint: metrics.controlPoint4];

    metrics.midpoint = [transformer transformPoint: metrics.midpoint];

    metrics.rawTransform = [transformer transformStruct];

    return (metrics);

} // calculateMetricsForStitch (inGrid)


// generate an oblong oval for the stitch the user has placed
// on the canvas.  This makes a vertical shape, which is then
// later drawn into a rotated coordinate system.  The two points
// are just sued for determining the 

+ (NSBezierPath *) makeStitchWithMetrics: (BWPathelizer_StitchMetrics) metrics
                        addControlPoints: (BOOL) addControlPoints
{
    // make the new path
    NSBezierPath *path;
    path = [NSBezierPath bezierPath];

    if (addControlPoints) {
        [path moveToPoint: metrics.pt1];
        [path appendBezierPathWithArcWithCenter: metrics.pt1
              radius: 5.0         startAngle: 0.0         endAngle: 360.0];
        [path closePath];

        [path moveToPoint: metrics.pt2];
        [path appendBezierPathWithArcWithCenter: metrics.pt2
              radius: 6.0         startAngle: 0.0         endAngle: 360.0];
        [path closePath];

        [path moveToPoint: metrics.pt3];
        [path appendBezierPathWithArcWithCenter: metrics.pt3
              radius: 7.0         startAngle: 0.0         endAngle: 360.0];
        [path closePath];

        [path moveToPoint: metrics.pt4];
        [path appendBezierPathWithArcWithCenter: metrics.pt4
              radius: 8.0         startAngle: 0.0         endAngle: 360.0];
        [path closePath];

        if (metrics.bendy) {
            [path moveToPoint: metrics.controlPoint1];
            [path appendBezierPathWithArcWithCenter: metrics.controlPoint1
                  radius: 5.0     startAngle: 0.0         endAngle: 360.0];
            [path closePath];
            
            [path moveToPoint: metrics.controlPoint2];
            [path appendBezierPathWithArcWithCenter: metrics.controlPoint2
                  radius: 6.0     startAngle: 0.0         endAngle: 360.0];
            [path closePath];
            
            [path moveToPoint: metrics.controlPoint3];
            [path appendBezierPathWithArcWithCenter: metrics.controlPoint3
                  radius: 7.0     startAngle: 0.0         endAngle: 360.0];
            [path closePath];
            
            [path moveToPoint: metrics.controlPoint4];
            [path appendBezierPathWithArcWithCenter: metrics.controlPoint4
                  radius: 8.0     startAngle: 0.0         endAngle: 360.0];
            [path closePath];
        }

        [path moveToPoint: metrics.startCenter];
        [path appendBezierPathWithArcWithCenter: metrics.startCenter
              radius: 2.0         startAngle: 0.0         endAngle: 360.0];
        [path closePath];

        [path moveToPoint: metrics.endCenter];
        [path appendBezierPathWithArcWithCenter: metrics.endCenter
              radius: 2.0         startAngle: 0.0         endAngle: 360.0];
        [path closePath];
    }

    // start at one corner of the rectangle
    [path moveToPoint: metrics.pt1];
    
    // add the top half-circle
    [path appendBezierPathWithArcWithCenter: metrics.startCenter
          radius: metrics.width
          startAngle: metrics.angle1
          endAngle: metrics.angle2];

    // == is probably OK here, since if the control point doesn't
    // exist, 0.0 is passed for these
    if (!metrics.bendy) {
        // move to the bottom part of the rectangle
        [path lineToPoint: metrics.pt3];
    } else {
        [path curveToPoint: metrics.pt3
              controlPoint1: metrics.controlPoint1
              controlPoint2: metrics.controlPoint2];
    }

    // add the bottom half-circle
    [path appendBezierPathWithArcWithCenter: metrics.endCenter
          radius: metrics.width
          startAngle: metrics.angle3
          endAngle: metrics.angle4];

    // complete the path
    if (!metrics.bendy) {
        // move to the bottom part of the rectangle
//      [path closePath];
        [path lineToPoint: metrics.pt1];
        // under some rotations, there's a tooth that sticks out from here.

    } else {
        [path curveToPoint: metrics.pt1
              controlPoint1: metrics.controlPoint3
              controlPoint2: metrics.controlPoint4];
    }

    return (path);

} // makeStitchWithMetrics


+ (BOOL) metricsAreDegenerate: (BWPathelizer_StitchMetrics) metrics
{
    BOOL degeneration = NO;

    if (metrics.pt1.x == metrics.pt2.x && metrics.pt1.y == metrics.pt2.y
        && metrics.pt3.x == metrics.pt4.x && metrics.pt3.y == metrics.pt4.y) {
        degeneration = YES;
    }

    // if something is nan (not-a-number), then we're definitely degenerate
    if (isnan(metrics.pt1.x) || isnan(metrics.pt1.y)
        || isnan(metrics.pt2.x) || isnan(metrics.pt2.y)
        || isnan(metrics.pt3.x) || isnan(metrics.pt3.y)
        || isnan(metrics.pt4.x) || isnan(metrics.pt4.y)) {
        degeneration = YES;
    }

    return (degeneration);

} // metricsAreDegenerate


+ (NSBezierPath *) makePathForStitch: (BWStitch *) stitch
                              inGrid: (BWGrid *) grid
{
    // vvv ---- cutten pasten ---- vvv

    BWGrid_Square startSquare, endSquare;
    BWGrid_SquarePart startPart, endPart;

    [stitch getStartSquare: &startSquare  part: &startPart];
    [stitch getEndSquare: &endSquare  part: &endPart];

    if (startSquare.x == endSquare.x 
        && startSquare.y == endSquare.y
        && startPart == endPart) {
    }

    float stitchWidth;
    stitchWidth = [stitch width];

    NSRect startRect, endRect;
    startRect = [grid rectForSquare: startSquare  part: startPart];
    endRect = [grid rectForSquare: endSquare  part: endPart];

    NSPoint startPoint, endPoint;
    startPoint = centerPoint (startRect);
    endPoint = centerPoint (endRect);

    float length;
    length = distance (startPoint, endPoint);

    // ^^^ ---- cutten pasten ---- ^^^

    // control point fooby
    float bendX = 0.0;
    float bendY = 0.0;
    float bendSpread = 0.0;

    float bendFactor;
    bendFactor = [stitch bendFactor];

    float bendDisplacement;
    bendDisplacement = [stitch bendDisplacement];

    if (bendFactor != 0.0) {
        bendX = length / 2.0 + bendDisplacement;
        bendY = bendFactor;

        bendSpread = [stitch bendFlatness];
    }

    BOOL showControlPoints;
    showControlPoints = [stitch displayDebugControlPoints];

    BWPathelizer_StitchMetrics metrics;
    metrics = [self calculateStitchMetricsWithLength: length
                    width: stitchWidth
                    bendX: bendX
                    bendY: bendY
                    bendSpread: bendSpread];

    NSBezierPath *path;


    if ([self metricsAreDegenerate: metrics]) {

        path = [NSBezierPath bezierPath];
        [path appendBezierPathWithArcWithCenter: metrics.startCenter
              radius: stitchWidth
              startAngle: 0.0
              endAngle: 360.0];

    } else {
        path = [self makeStitchWithMetrics: metrics
                     addControlPoints: showControlPoints];
    }
    
    // vvv ---- cutten pasten from above ---- vvv

    NSAffineTransform *transformer; // robots in disguise
    transformer = [NSAffineTransform transform];
    
    // move the origin to the start point
    [transformer translateXBy: startPoint.x + 0.5  yBy: startPoint.y + 0.5];

    // if they're the same, you get a NaN, which really confuses things
    if (startPoint.x != endPoint.x
        || startPoint.y != endPoint.y) {
        float angle;
        angle = angleForPoints (startPoint, endPoint);
        [transformer rotateByRadians: angle];
    }

    [path transformUsingAffineTransform: transformer];

    // ^^^ ---- cutten pasten ---- ^^^

    return (path);

} // makePathForStitch
    


+ (NSBezierPath *) makeArrowPathForStitch: (BWStitch *) stitch
                                   inGrid: (BWGrid *) grid
{
    BWStitch_ArrowKind arrow;
    arrow = [stitch arrow]; // eventually we'll have different styles

    // vvv ---- copied verbatim from above ---- vvv

    BWGrid_Square startSquare, endSquare;
    BWGrid_SquarePart startPart, endPart;

    [stitch getStartSquare: &startSquare  part: &startPart];
    [stitch getEndSquare: &endSquare  part: &endPart];

    float stitchWidth;
    stitchWidth = [stitch width];

    NSRect startRect, endRect;
    startRect = [grid rectForSquare: startSquare  part: startPart];
    endRect = [grid rectForSquare: endSquare  part: endPart];

    NSPoint startPoint, endPoint;
    startPoint = centerPoint (startRect);
    endPoint = centerPoint (endRect);

    float length;
    length = distance (startPoint, endPoint);

    // ^^^ ---- copied verbatim from above ---- ^^^


    float arrowLength;
    arrowLength = [stitch arrowLength];

    float start;
    start = (length / 2.0) - (arrowLength / 2.0);

    NSPoint arrowStart, arrowEnd;
    arrowStart = NSMakePoint (start, 0.0);
    arrowEnd = NSMakePoint (start + arrowLength, 0.0);

    NSBezierPath *path;
    path = [NSBezierPath bezierPath];

    [path moveToPoint: arrowStart];
    [path lineToPoint: arrowEnd];

    NSPoint ook, ack;
    ook = arrowEnd;
    ook.x -= 16;
    ook.y += 5;

    [path moveToPoint: arrowEnd];
    [path lineToPoint: ook];

    ack = arrowEnd;
    ack.x -= 16;
    ack.y -= 5;

    [path moveToPoint: arrowEnd];
    [path lineToPoint: ack];

    float weight;
    weight = [stitch arrowWeight];
    [path setLineWidth: weight];

    [path setLineJoinStyle: NSMiterLineJoinStyle];


    // vvv ---- copied verbatim from above ---- vvv

    NSAffineTransform *transformer; // robots in disguise
    transformer = [NSAffineTransform transform];
    
    // move the origin to the start point
    [transformer translateXBy: startPoint.x + 0.5  yBy: startPoint.y + 0.5];
    
    float angle;
    angle = angleForPoints (startPoint, endPoint);

    [transformer rotateByRadians: angle];

    [path transformUsingAffineTransform: transformer];

    // ^^^ ---- copied verbatim from above ---- ^^^

    return (path);

} // makeArrowPathForStitch


@end // BWPathelizer

